YownYang's blog

查找删除iOS项目中未使用的代码文件

本篇博客主要是介绍构建一个Mac项目,用来对项目中未使用(冗余)的代码文件进行查找和删除。

前言

5月份的时候入职了一家新公司,入职之后发现项目混乱,问题很多。开始的时候并未注意到项目中有很多随着版本迭代和设计者不合理设计遗留的文件,随着对项目逐渐深入发现项目中有很多冗余文件,决定写个工具进行查找删除。

项目的组成

首先,我们需要了解一个项目中各种东西的概念。

Xcode project

Xcode 中的project是指一个项目,它包含各种文件。它可以进行各种项目级别的配置,如debug和release,也可以配置多种target,项目本身是无法产生app或者mac应用的。

* .h/.m/.mm/.c等源代码文件
* .a/framework等库文件
* image/bundle/plist等资源文件
* xib/storyboard等特殊资源文件

Xcode target

target定义了构建一个app所需的文件和配置。target之间可以互相依赖,当它们在同一个workspace中时,Xcode会发现它们的隐性依赖关系。你也可以对它们进行设置,使其显性依赖。

Xcode workspace

workspace一般直译为工作空间,这个概念在苹果平台开发上面不是很明显,因为我们都是通过xxx.xcodeproj文件直接打开项目进行工作的(其实也是存在workspace的),但如果你使用cocoapods进行第三方库的管理,你会发现你只有通过打开xxx.xcworkspace文件才能正常工作。

这是因为cocoapods产生了一个project用来存放各种第三方库产生的target,又将主项目的project和自己产生的project放在同一个workspace下进行依赖。

Xcode scheme

既然说了这么多了,也不介意再提及下scheme这个东西。它可以包含多个target进行执行,但每次只能执行一个scheme。它可以存在project下,也可以存在workspace下。前者可以让包含了project的任意workspace使用,后者只能让那个workspace本身使用了。

项目组成的总结

它们的关系大概可以这样总结:

workspace > project > scheme > target

或者:

workspace > scheme > project > target

正常工作项目的分析

通过上面的说明,其实可以知道了,我们如果要找出一个项目中文件的组织关系,应该去project里面找。对于只存在一个project的项目,应该去xxx.xcodeproj中查找关系。对于使用了cocoapods的,如果要查找cocoapods项目的文件关系,可以去pod.xcodeproj中查找。不过一般没有必要,因为里面存的都是库,并且数量不会很多,不使用的话一般是整个库的移除。

xxx.xcodeproj

xxx.xcodeproj其实是一个文件夹,使用右键打开包内容,你会发现它包含了一个叫做project.pbxproj的文件,这个文件才是用来保存project中文件的组织关系的。

project.pbxproj

它其实是一个老式的plist文件,它里面的每一个元素都是一个24位长度的十六进制标识符。这标识符似乎基于日期,序列和预定义值生成,所以保证了它的唯一性。(PS: 在看这个文件的时候建议使用TextMate,将格式指定为Property List(Old-Style))。

它最外层包含5个键值对,key分别是:archiveVersionclassesobjectVersionobjectsrootObject。我们只需要关心objectsrootObject就行了。

解析objects和rootObject(附代码)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
// 1. 获取objects字典,fullDictionary是整个project.pbxproj的内容
NSDictionary *objectDictionary = fullDictionary[@"objects"];
// 2. 获取project的key
NSString *rootObjectKey = fullDictionary[@"rootObject"];
// 3. 根据key从objects字典中获取project的字典
NSDictionary *projectDictionary = objectDictionary[rootObjectKey];
// 4. 获取mainGroup的key
NSString *mainGroupKey = projectDictionary[@"mainGroup"];
// 5. 根据key从objects字典中获取mainGroup的字典
NSDictionary *mainGroupDictionary = objectDictionary[mainGroupKey];
// 6. 获取children数组,实际就是主target最外层的所有文件夹和文件
NSArray *floderArray = mainGroupDictionary[@"children"];
// 7. 以递归的方式获取项目中所有的h/m/mm/xib文件,将它们存储在一个字典套字典中。内层字典是每个文件自身的唯一标识做key,它们自身的完整路径做value。外层字典以每个不带后缀的文件名做key,以内层字典为value。
- (void)searchAllClassWithArray:(NSArray *)keyArray objectDictionary:(NSDictionary *)objectDictionary path:(NSString *)path {
[keyArray enumerateObjectsUsingBlock:^(NSString *key, NSUInteger idx, BOOL * _Nonnull stop) {
@autoreleasepool {
NSDictionary *floderOrFileDictionary = objectDictionary[key];
NSString *type = floderOrFileDictionary[@"isa"];
NSString *onePath = floderOrFileDictionary[@"path"];
NSString *floderOrFilePath = [path stringByAppendingPathComponent:onePath];
if ([type isEqualToString:@"PBXFileReference"]) {
// 如果是非实体文件夹中的文件,会带有非实体文件夹的路径,所以要取最后一段
NSString *className = onePath.lastPathComponent;
NSString *classExtension = className.pathExtension;
if ([classExtension isEqualToString:@"h"] ||
[classExtension isEqualToString:@"m"] ||
[classExtension isEqualToString:@"xib"] ||
[classExtension isEqualToString:@"mm"]) {
NSString *classNameWithoutExtension = className.stringByDeletingPathExtension;
NSMutableDictionary *tempClassDictionary = [self.allClassDictionary objectForKey:classNameWithoutExtension];
if (tempClassDictionary) {
[tempClassDictionary setObject:floderOrFilePath forKey:key];
} else {
tempClassDictionary = [NSMutableDictionary dictionary];
[tempClassDictionary setObject:floderOrFilePath forKey:key];
[self.allClassDictionary setObject:tempClassDictionary forKey:classNameWithoutExtension];
}
}
} else if ([type isEqualToString:@"PBXGroup"]) {
NSArray *floderArray = floderOrFileDictionary[@"children"];
[self searchAllClassWithArray:floderArray objectDictionary:objectDictionary path:floderOrFilePath];
}
}
}];
}
// 8. 枚举所有的文件,从中匹配是否有导入其他的.h文件。如果有,则将那个文件簇标记为使用文件。
- (void)searchUsedClass {
NSMutableDictionary *tempAllClassDictionary = [NSMutableDictionary dictionary];
[self.allClassDictionary.allValues enumerateObjectsUsingBlock:^(NSMutableDictionary *obj, NSUInteger idx, BOOL * _Nonnull stop) {
@autoreleasepool {
[tempAllClassDictionary addEntriesFromDictionary:obj];
}
}];
[tempAllClassDictionary enumerateKeysAndObjectsUsingBlock:^(NSString *classUUID, NSString *classPath1, BOOL * _Nonnull stop) {
@autoreleasepool {
NSString *classExtension = classPath1.pathExtension;
if ([classExtension isEqualToString:@"h"]) {
NSString *className1 = classPath1.lastPathComponent;
NSString *classNameWithoutExtension1 = className1.stringByDeletingPathExtension;
[tempAllClassDictionary enumerateKeysAndObjectsUsingBlock:^(NSString *classUUID, NSString *classPath2, BOOL * _Nonnull stop) {
@autoreleasepool {
// 不能从自己的类中匹配
NSString *className2 = classPath2.lastPathComponent;
NSString *classNameWithoutExtension2 = className2.stringByDeletingPathExtension;
if (![classNameWithoutExtension1 isEqualToString:classNameWithoutExtension2]) {
NSString *searchTypeOne = [NSString stringWithFormat:@"#import \"%@\"", className1];
NSString *searchTypeTwo = [NSString stringWithFormat:@"\"%@\"", classNameWithoutExtension1];
NSString *contents = [NSString stringWithContentsOfFile:classPath2 encoding:NSUTF8StringEncoding error:nil];
BOOL isContainTypeOne = [contents containsString:searchTypeOne];
BOOL isContainTypeTwo = [contents containsString:searchTypeTwo];
if (isContainTypeOne || isContainTypeTwo) {
[self.usedClassDictionary setObject:classPath1 forKey:classNameWithoutExtension1];
}
}
}
}];
}
}
}];
}
// 9. 使用所有文件的数组去除掉使用文件,剩下的就是未使用文件了。也可以在这里做一些忽略过滤,过滤掉不想删除的文件。如main.m。
- (void)searchUnusedClass {
self.unusedClassDictionary = [NSMutableDictionary dictionaryWithDictionary:self.allClassDictionary];
[self.unusedClassDictionary removeObjectsForKeys:self.usedClassDictionary.allKeys];
[self.unusedClassDictionary removeObjectForKey:@"main"];
}

总结

拉了这么长的篇幅,写了这么多代码,还是不如完整项目来的好。
仓库地址

参考文献

  1. 苹果官方文档
  2. project.pbxproj